{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit UIControls;

interface

uses Classes, AHMTypes, BaseControls, Viewport, PlayList, TextEntry, Scrolling,
     GridObjects, PluginManager, Effects, UIObjects;

type
  // Display controls

  TAHMCaptionControl = class(TAHMControl)
  public
    constructor Create(const Name: String); override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMImageControl = class(TAHMControl)
  public
    constructor Create(const Name: String); override;
    procedure Rendering; override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMShapeControl = class(TAHMControl)
  public
    constructor Create(const Name: String); override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMGaugeControl = class(TAHMControl)
  protected
    function GetMaximum: Integer; virtual;
    function GetPosition: Integer; virtual;
    procedure SetMaximum(Maximum: Integer); virtual;
    procedure SetPosition(Position: Integer); virtual;
  public
    constructor Create(const Name: String); override;
    property Maximum: Integer read GetMaximum write SetMaximum;
    property Position: Integer read GetPosition write SetPosition;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMCloneControl = class(TAHMImmutableControl)
  private
    FHostViewport: TAHMViewport;
    FHostVerticalFlip: Boolean;
    FHostHorizontalFlip: Boolean;
    FHostXScale: Single;
    FHostYScale: Single;
    FHostIdleEffect: TAHMEffect;
    FHostFocusedEffect: TAHMEffect;
    FHostPressedEffect: TAHMEffect;
    FHostCheckedEffect: TAHMEffect;
  protected
    procedure SetAncestor(Ancestor: TAHMUIObject); override;
    function GetStyles: TAHMUIObjectList; override;
    procedure Assimilate(Host: TAHMControl);
    procedure Dissimilate(Host: TAHMControl);
  public
    constructor Create(const Name: String); override;
    procedure Render(Viewport: TAHMViewport); override;
    procedure BuildProperties(Properties: TAHMUIPropertyList); override;
  end;

  // Interactive Controls

  TAHMMeterControl = class(TAHMInteractiveControl)
  protected
    FPositionCache: Integer;
    FRenderValue: Single;
    function GetMaximum: Integer; virtual;
    function GetPosition: Integer; virtual;
    procedure SetMaximum(Maximum: Integer); virtual;
    procedure SetPosition(Position: Integer); virtual;
  public
    property Maximum: Integer read GetMaximum write SetMaximum;
    property Position: Integer read GetPosition write SetPosition;
    property RenderValue: Single read FRenderValue;
    procedure Press; override;
    procedure MouseClicked; override;
  end;

  TAHMDialControl = class(TAHMMeterControl)
  protected
    procedure Loaded; override;
  public
    constructor Create(const Name: String); override;
    procedure MouseMoved(Down: Boolean); override;
    procedure Rendering; override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMBarControl = class(TAHMMeterControl)
  public
    constructor Create(const Name: String); override;
    procedure MouseMoved(Down: Boolean); override;
    procedure Rendering; override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMMediaControl = class(TAHMInteractiveControl)
  public
    constructor Create(const Name: String); override;
    function Scroll(Direction: TAHMDirection): Boolean; override;
    procedure Activate; override;
    procedure Cancel; override;
    procedure MouseClicked; override;
    procedure MouseMoved(Down: Boolean); override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMPluginControl = class(TAHMInteractiveControl)
  private
    FPlugin: TAHMPlugin;
  public
    constructor Create(const Name: String); override;
    property Plugin: TAHMPlugin read FPlugin write FPlugin;
    procedure SendKey(KeyCode: String); override;
    procedure SendKey(KeyCode: Word; Shift: TShiftState); override;
    procedure Press; override;
    procedure UnPress; override;
    procedure MouseMoved(Down: Boolean); override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMButtonControl = class(TAHMInteractiveControl)
  public
    constructor Create(const Name: String); override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMComboButtonControl = class(TAHMInteractiveControl)
  public
    constructor Create(const Name: String); override;
    procedure Activate; override;
    procedure SeedObject; override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMEditableControl = class;

  TAHMControlTextParser = class(TAHMTextEntryParser)
  private
    FControl: TAHMEditableControl;
  protected
    procedure Enter; override;
  public
    constructor Create(Control: TAHMEditableControl);
  end;

  // Editable Interactive Controls
  TAHMEditableControl = class(TAHMInteractiveControl)
  private
    FDirtyText: Boolean;
    FTextParser: TAHMControlTextParser;
    procedure UpdateTextParser;
  protected
    property Parser: TAHMControlTextParser read FTextParser;
    function GetDisplayText: String; override;
    procedure Loaded; override;
    procedure TextEdited; virtual;
  public
    constructor Create(const Name: String); override;
    destructor Destroy; override;
    procedure SendKey(KeyCode: String); override;
    procedure SendKey(KeyCode: Word; Shift: TShiftState); override;
  end;

  TAHMTextEditControl = class(TAHMEditableControl)
  public
    constructor Create(const Name: String); override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  // Checkable Interactive Controls
  TAHMCheckButtonControl = class(TAHMComboButtonControl)
  protected
    function GetChecked: Boolean; override;
    procedure SetChecked(Checked: Boolean); override;
  public
    constructor Create(const Name: String); override;
    procedure SeedObject; override;
    procedure SaveAsXML(Lines: TStrings); override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMRadioButtonControl = class(TAHMInteractiveControl)
  protected
    function GetDisplayMeta: String; override;
    function GetChecked: Boolean; override;
    procedure SetChecked(Checked: Boolean); override;
  public
    constructor Create(const Name: String); override;
    procedure SeedObject; override;
    procedure Activate; override;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  // Grid Controls
  TAHMGridControl = class(TAHMInteractiveControl)
  private
    FGridObjects: TAHMGridObjectList;
    FRowCount: Integer;
    FColCount: Integer;
    FTopRow: Integer;
    FLeftCol: Integer;
    FRowOffset: Single;
    FColOffset: Single;
    FSelectedRow: Integer;
    FSelectedCol: Integer;
    FLoadedContents: Boolean;
    FVerticalScroll: Boolean;
    FScrollBraking: Boolean;
    FScrollSpeed: Single;
    FPrivateScroller: TAHMAbstractScroller;
  protected
    function GetChecked: Boolean; override;
    procedure SetChecked(Checked: Boolean); override;
    procedure RefreshGridContents;
    procedure PopulateGridContents; virtual;
    procedure Loaded; override;
    procedure InvalidateStyle; override;
    procedure InitialiseEffect; override;
    procedure RenderEffects; override;
    procedure SetGridObject(ItemIndex: String; Validate: Boolean = false);
    function GetGridObject(Row, Column: Integer): TAHMGridObject;
    function UpdateMouseSelection: Boolean;
    function UpdateSelection(Row, Column: Integer): Boolean; virtual;
    function ScrollToSelection(Row, Column: Integer): Boolean;
    procedure SnapToPosition;
    procedure UpdateSmoothScrolling;
    procedure FireScrollActions;
  public
    constructor Create(const Name: String); override;
    destructor Destroy; override;
    property LoadedContents: Boolean read FLoadedContents;
    property GridObjects: TAHMGridObjectList read FGridObjects;
    property RowCount: Integer read FRowCount;
    property ColumnCount: Integer read FColCount;
    property SelectedRow: Integer read FSelectedRow;
    property SelectedCol: Integer read FSelectedCol;
    property TopRow: Integer read FTopRow;
    property LeftCol: Integer read FLeftCol;
    property RowOffset: Single read FRowOffset;
    property ColOffset: Single read FColOffset;
    property GridObject[Row, Column: Integer]: TAHMGridObject read GetGridObject;
    function PageScroll(Direction: TAHMDirection): Boolean; override;
    function Scroll(Direction: TAHMDirection): Boolean; override;
    function SmoothScroll(DX, DY: Single): Boolean; override;
    procedure Activate; override;
    procedure MouseClicked; override;
    procedure MouseMoved(Down: Boolean); override;
    procedure MouseWheel(Delta: Integer); override;
    procedure Idle; override;
    procedure Invalidate; override;
    procedure Rendering; override;
    procedure Rendered; override;
    procedure UpdateScroller(FocusedItem: Boolean);
    class function GetStyleList: TAHMUIObjectList; override;
  end;

  TAHMMediaGridControl = class(TAHMGridControl)
  private
    FFolder: WideString;
    FPrevFolder: WideString;
    FMediaType: TAHMMediaType;
    function GetMediaList: TAHMPlaylist;
  protected
    property MediaList: TAHMPlaylist read GetMediaList;
    procedure RetrieveMediaItems;
    procedure PopulateGridContents; override;
    function UpdateSelection(Row, Column: Integer): Boolean; override;
  public
    constructor Create(const Name: String); override;
    procedure Activate; override;
    procedure Cancel; override;
    procedure Parent;
    procedure Root;
    class function GetStyleList: TAHMUIObjectList; override;
  end;

implementation

uses SysUtils, Math, Bars, Actions, Functions, UIGrids, MediaGrids, Dials,
     Timing, ApplicationManager, MediaManager, MetaConsts, TagNames, UniCode,
     Screen, Colour, Bitmaps;

const
  // Inertia scrolling constants
  MAX_SPEED = 0.300; // Maximum scrolling speed (screens/frame)
  MIN_SPEED = 0.001; // Minimum scrolling speed - any slower and we stop
  ACCEL_FACTOR = 0.50; // Acceleration multiplier for mouse movement
  DECEL_FACTOR = 0.15; // Deceleration multiplier to simulate friction
  DECEL_SPEED = 0.00; // Deceleration constant to simulate friction
  EXPECTED_INTERVAL = 50; // Base frame interval for inertia calculations


constructor TAHMCaptionControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_CAPTION;
  ImageIndex := Ord(ctCaption);

  // Create a text scroller (free'd by ancestor class)
  Scroller := TAHMTextScroller.Create;
end;

class function TAHMCaptionControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.CaptionList;
end;

//----------------------------------------------------------------------------//

constructor TAHMImageControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_IMAGE;
  ImageIndex := Ord(ctImage);
end;

procedure TAHMImageControl.Rendering;
var
  URL: WideString;
begin
  inherited;

  // We need to check for any external changes to our image URL
  if (MetaIndex <> '') and (DefaultStyle <> nil) then
  begin
    URL := ApplicationManagerInstance.MetaDataManager.GetMetaWideValue(MetaIndex);
    if URL <> '' then TAHMBitmap(DefaultStyle).FileURL := URL;
  end;
end;

class function TAHMImageControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.BitmapList;
end;

//----------------------------------------------------------------------------//

constructor TAHMShapeControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_SHAPE;
  ImageIndex := Ord(ctShape);
end;

class function TAHMShapeControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.ShapeList;
end;

//----------------------------------------------------------------------------//

constructor TAHMGaugeControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_GAUGECONTROL;
  ImageIndex := Ord(ctGauge);
end;

function TAHMGaugeControl.GetMaximum: Integer;
begin
  // Maximum value defined by MetaGroup
  if MetaGroup <> '' then
    Result := ApplicationManagerInstance.MetaDataManager.GetMetaInteger(MetaGroup)
  else
    Result := 0;
end;

function TAHMGaugeControl.GetPosition: Integer;
begin
  // Position value defined by MetaValue
  if MetaValue <> '' then
    Result := ApplicationManagerInstance.MetaDataManager.GetMetaInteger(MetaValue)
  else
    Result := 0;
end;

procedure TAHMGaugeControl.SetMaximum(Maximum: Integer);
begin
  if MetaGroup <> '' then
    ApplicationManagerInstance.MetaDataManager.SetMetaInteger(MetaGroup, Maximum);
end;

procedure TAHMGaugeControl.SetPosition(Position: Integer);
begin
  if MetaValue <> '' then
    ApplicationManagerInstance.MetaDataManager.SetMetaInteger(MetaValue, Position);
end;

class function TAHMGaugeControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.GaugeList;
end;

//----------------------------------------------------------------------------//

constructor TAHMCloneControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_CLONE;
  ImageIndex := Ord(ctClone);

  // Override default properties
  Enabled := False;
  MouseEnabled := False;
end;

procedure TAHMCloneControl.SetAncestor(Ancestor: TAHMUIObject);
begin
  // Copy/Clear style if ancestor is changed
  if Self.Ancestor <> Ancestor then
    if Ancestor is TAHMControl then Style := TAHMControl(Ancestor).Style
                               else Style := nil;

  inherited;
end;

function TAHMCloneControl.GetStyles: TAHMUIObjectList;
begin
  // Clones only allow their donor ancestor style types to be selected
  if Ancestor is TAHMControl then Result := TAHMControl(Ancestor).Styles
                             else Result := nil;
end;

procedure TAHMCloneControl.Assimilate(Host: TAHMControl);
begin
  // Backup host properties before we override them
  FHostViewPort := Host.Viewport;
  FHostVerticalFlip := Host.VerticalFlip;
  FHostHorizontalFlip := Host.HorizontalFlip;
  FHostXScale := Host.OwnXScale;
  FHostYScale := Host.OwnYScale;
  FHostIdleEffect := Host.Effect;
  if Host is TAHMInteractiveControl then
  begin
    FHostFocusedEffect := TAHMInteractiveControl(Host).FocusedEffect;
    FHostPressedEffect := TAHMInteractiveControl(Host).PressedEffect;
    FHostCheckedEffect := TAHMInteractiveControl(Host).CheckedEffect;
  end;

  // Apply our custom properites to host for rendering
  Host.Viewport := Self.Viewport;
  Host.VerticalFlip := Self.VerticalFlip;
  Host.HorizontalFlip := Self.HorizontalFlip;
  Host.OwnXScale := Self.OwnXScale;
  Host.OwnYScale := Self.OwnYScale;
  Host.Effect := Self.Effect;
  if Host is TAHMInteractiveControl then
  begin
    TAHMInteractiveControl(Host).FocusedEffect := Self.FocusedEffect;
    TAHMInteractiveControl(Host).PressedEffect := Self.PressedEffect;
    TAHMInteractiveControl(Host).CheckedEffect := Self.CheckedEffect;
  end;
end;

procedure TAHMCloneControl.Dissimilate(Host: TAHMControl);
begin
  // Restore overridden host properties after rendering
  Host.Viewport := FHostViewport;
  Host.VerticalFlip := FHostVerticalFlip;
  Host.HorizontalFlip := FHostHorizontalFlip;
  Host.OwnXScale := FHostXScale;
  Host.OwnYScale := FHostYScale;
  Host.Effect := Self.Effect;
  if Host is TAHMInteractiveControl then
  begin
    TAHMInteractiveControl(Host).FocusedEffect := FHostFocusedEffect;
    TAHMInteractiveControl(Host).PressedEffect := FHostPressedEffect;
    TAHMInteractiveControl(Host).CheckedEffect := FHostCheckedEffect;
  end;
end;

procedure TAHMCloneControl.Render(Viewport: TAHMViewport);
var
  Clone: TAHMControl;
begin
  // Keep a copy of passed viewport object
  FViewport := Viewport;

  // Apply rotation transformation to control
  with Viewport.Canvas do
    if (FXRot <> 0) or (FYRot <> 0) or (FZRot <> 0) then
      SetRotation(FXRot, FYRot, FZRot, FCentreX, FCentreY)
    else
      UnsetRotation;

  // Call rendering method to initialise control
  Rendering;

  // We must have an ancestor defined for cloning and we can't clone a clone
  if (Ancestor is TAHMControl) and not (Ancestor is TAHMCloneControl) then
    Clone := TAHMControl(Ancestor)
  else
    Clone := nil;

  // Perform standard control rendering on clone
  if Visible then
  begin
    // Render cloned control using our style and position
    if Assigned(Clone) and Clone.Visible then
    begin
      Assimilate(Clone);
      Clone.Rendering;

      // Render control using associated style
      if Assigned(DefaultStyle) then DefaultStyle.Render(Clone, XPos, YPos, Width, Height);

      Clone.Rendered;
      Dissimilate(Clone);
    end;

    // Render borders for display in designer
    if DrawBorder then
      with ScreenInstance.Viewport.Canvas do
      begin
        PenWidth := 0.004;
        PenColour := TAHMCanvasColour.Black;
        Rectangle(ClipRect, Gamma, XScale, YScale);
        PenWidth := 0.002;
        PenColour := TAHMCanvasColour.White;
        Rectangle(ClipRect, Gamma, XScale, YScale);
      end;
  end;
end;

procedure TAHMCloneControl.BuildProperties(Properties: TAHMUIPropertyList);
begin
{$IFDEF DESIGNER}
  inherited;

  // Build custom clone control properties
  Properties.Remove('Text');
  Properties.Remove('Hint');
  Properties.Remove('MetaValue');
  Properties.Remove('MetaGroup');
  Properties.Remove('MetaIndex');
  Properties.Remove('Actions');
  Properties.Remove('InitiallyEnabled');
  Properties.Remove('InitiallyMouseEnabled');
  Properties.Remove('NavigateUpControl');
  Properties.Remove('NavigateDownControl');
  Properties.Remove('NavigateLeftControl');
  Properties.Remove('NavigateRightControl');
{$ENDIF}
end;

//----------------------------------------------------------------------------//

function TAHMMeterControl.GetMaximum: Integer;
begin
  // Maximum value defined by MetaGroup
  if MetaGroup <> '' then
    Result := ApplicationManagerInstance.MetaDataManager.GetMetaInteger(MetaGroup)
  else
    Result := 0;
end;

function TAHMMeterControl.GetPosition: Integer;
begin
  // Position value defined by MetaValue
  if MetaValue <> '' then
    Result := ApplicationManagerInstance.MetaDataManager.GetMetaInteger(MetaValue)
  else
    Result := 0;
end;

procedure TAHMMeterControl.SetMaximum(Maximum: Integer);
begin
  if MetaGroup <> '' then
    ApplicationManagerInstance.MetaDataManager.SetMetaInteger(MetaGroup, Maximum);
end;

procedure TAHMMeterControl.SetPosition(Position: Integer);
begin
  if MetaValue <> '' then
  begin
    ApplicationManagerInstance.MetaDataManager.SetMetaInteger(MetaValue, Position);
    FPositionCache := Position;
  end;
end;

procedure TAHMMeterControl.Press;
begin
  inherited;

  // Ensure we update meter as soon as mouse is held down
  MouseMoved(True);
end;

procedure TAHMMeterControl.MouseClicked;
begin
  // Suppress default actions for meter control when clicked
end;

//----------------------------------------------------------------------------//

constructor TAHMDialControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_DIALCONTROL;
  ImageIndex := Ord(ctDial);
end;

procedure TAHMDialControl.Loaded;
begin
  inherited;

  // Initialise start angle for dial
  if Assigned(DefaultStyle) then FRenderValue := TAHMDial(DefaultStyle).StartAngle;
end;

procedure TAHMDialControl.MouseMoved(Down: Boolean);
var
  iMax, iPos, iOff, iOld, MaxAngle: Integer;
  NewAngle, AngleVal, DeltaX, DeltaY: Single;
begin
  inherited;

  // Only update position whilst mouse is held down
  if Down then
  begin
    with DefaultStyle as TAHMDial do
    begin
      // Sanity check size to prevent division by zero
      if (FWidth = 0) or (FHeight = 0) or (SweepAngle = 0) then Exit;

      // Calculate angle of dial knob
      DeltaX := RotateCenterX - MouseX;
      DeltaY := RotateCenterY - MouseY;
      NewAngle := RadToDeg(ArcTan2(DeltaY, DeltaX)) + 180;
      if NewAngle > 360 then NewAngle := NewAngle - 360;

      // Initialise calculated position - out of range
      AngleVal := 2;
      MaxAngle := StartAngle + SweepAngle;

      // Check for rotation angle in required dial range
      if MaxAngle > 360 then
      begin
        if (NewAngle > StartAngle) and (NewAngle < 360) then
          AngleVal := (NewAngle - StartAngle) / SweepAngle
        else if (NewAngle > 0) and (NewAngle < (MaxAngle mod 360)) then
          AngleVal := (360 - StartAngle + NewAngle) / SweepAngle;
      end
      else if (NewAngle > StartAngle) and (NewAngle < MaxAngle) then
        AngleVal := (NewAngle - StartAngle) / SweepAngle;

      if AngleVal < 2 then
      begin
        // Update angle for rendering
        FRenderValue := NewAngle;

        // Get maximum dial position from meta group property
        iMax := GetMaximum;
        if iMax = 0 then Exit;

        // Do we need to handle negative positions?
        if iMax < 0 then iOff := Abs(iMax) else iOff := 0;

        // Apply negative offsets whilst calculating position
        iMax := iMax + iOff + iOff;
        iPos := Round(iMax * AngleVal);

        // Clip minimum & maximum values then remove negative offset
        if iPos < 0 then iPos := 0;
        if iPos > iMax then iPos := iMax;
        iPos := iPos - iOff;

        // Update new position and fire actions if changed
        iOld := GetPosition;
        if iPos <> iOld then
          with ApplicationManagerInstance.ActionManager do
          begin
            SetPosition(iPos);
            ProcessAction(AC_ONUPDATE);
            if iPos > iOld then ProcessAction(AC_ONINCREASE)
                           else ProcessAction(AC_ONDECREASE);
          end;
      end;
    end;
  end;
end;

procedure TAHMDialControl.Rendering;
var
  iMax, iPos: Integer;
begin
  inherited;

  // Update angle in response to external changes to metadata
  iPos := GetPosition;
  if iPos <> FPositionCache then
  begin
    // Get maximum dial position from meta group property
    iMax := GetMaximum;
    if iMax = 0 then Exit;

    if Assigned(DefaultStyle) then
      with DefaultStyle as TAHMDial do
        FRenderValue := StartAngle + Abs(SweepAngle * iPos / iMax);

    // Update cache with new position
    FPositionCache := iPos;
  end;
end;

class function TAHMDialControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.DialList;
end;

//----------------------------------------------------------------------------//

constructor TAHMBarControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_BAR;
  ImageIndex := Ord(ctBar);
end;

procedure TAHMBarControl.MouseMoved(Down: Boolean);
var
  iMax, iPos, iOff, iOld: Integer;
  SlideVal: Single;
begin
  inherited;

  // Sanity check size to prevent division by zero
  if (FWidth = 0) or (FHeight = 0) then Exit;

  // Only update position whilst mouse is held down
  if Down then
  begin
    // Determine whether bar should be scrolled vertically or horizontally
    if FWidth > FHeight then SlideVal := MouseX / FWidth
                        else SlideVal := MouseY / FHeight;

    // Get maximum bar position from meta group property
    iMax := GetMaximum;
    if iMax = 0 then Exit;

    // Do we need to handle negative positions?
    if iMax < 0 then iOff := Abs(iMax) else iOff := 0;

    // Apply negative offsets whilst calculating position
    iMax := iMax + iOff + iOff;
    FRenderValue := iMax * SlideVal;

    // Clip minimum & maximum values then remove negative offset
    if FRenderValue < 0 then FRenderValue := 0;
    if FRenderValue > iMax then FRenderValue := iMax;
    FRenderValue := FRenderValue - iOff;

    // For sliders we need to invert the calculated position
    if TAHMAbstractBar(DefaultStyle).BarType = btSlider then
      FRenderValue := GetMaximum - FRenderValue;
    iPos := Round(FRenderValue);

    // Update new position and fire actions if changed
    iOld := GetPosition;
    if iPos <> iOld then
      with ApplicationManagerInstance.ActionManager do
      begin
        SetPosition(iPos);
        ProcessAction(AC_ONUPDATE);
        if iPos > iOld then ProcessAction(AC_ONINCREASE)
                       else ProcessAction(AC_ONDECREASE);
      end;
  end;
end;

procedure TAHMBarControl.Rendering;
var
  iPos: Integer;
begin
  inherited;

  // Update bar in response to external changes to metadata
  iPos := GetPosition;
  if iPos <> FPositionCache then
  begin
    // Update bar and cache with new position
    FRenderValue := iPos;
    FPositionCache := iPos;
  end;
end;

class function TAHMBarControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.BarList;
end;

//----------------------------------------------------------------------------//

constructor TAHMMediaControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_MEDIACONTROL;
  ImageIndex := Ord(ctMediaWindow);
end;

procedure TAHMMediaControl.Activate;
var
  Stream: TAHMMediaStream;
begin
  inherited;

  // Override selection behaviour for clicking DVD menu current button
  Stream := ApplicationManagerInstance.MediaManager.ActiveStream;
  if Assigned(Stream) and Stream.MenuActive then
    Stream.MenuSelect;
end;

procedure TAHMMediaControl.Cancel;
var
  Stream: TAHMMediaStream;
begin
  inherited Cancel;

  // Override cancel behaviour to return from current DVD submenu
  Stream := ApplicationManagerInstance.MediaManager.ActiveStream;
  if Assigned(Stream) and Stream.MenuActive then
    Stream.MenuReturn;
end;

function TAHMMediaControl.Scroll(Direction: TAHMDirection): Boolean;
var
  Stream: TAHMMediaStream;
begin
  // Set default result - not scrolled
  Result := False;

  // Override cursor behaviour for navigating DVD menus
  Stream := ApplicationManagerInstance.MediaManager.ActiveStream;
  if Assigned(Stream) and Stream.MenuActive then
  begin
    Stream.MenuNavigate(Direction);
    DidSomething;
  end;
end;

procedure TAHMMediaControl.MouseClicked;
var
  Stream: TAHMMediaStream;
begin
  // Are we currently rendering a DVD menu?
  Stream := ApplicationManagerInstance.MediaManager.ActiveStream;
  if Assigned(Stream) and Stream.MenuActive then
  begin
    // Allow click event in DVD menu screens
    Stream.MenuSelect(MouseX, MouseY);
  end
  else
    inherited;
end;

procedure TAHMMediaControl.MouseMoved(Down: Boolean);
var
  Stream: TAHMMediaStream;
begin
  // Are we currently rendering a DVD menu?
  Stream := ApplicationManagerInstance.MediaManager.ActiveStream;
  if Assigned(Stream) and Stream.MenuActive then
  begin
    // Allow mouse tracking event in DVD menu screens
    Stream.MenuNavigate(MouseX, MouseY);
  end
  else
    inherited;
end;

class function TAHMMediaControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.MediaWindowList;
end;

//----------------------------------------------------------------------------//

constructor TAHMPluginControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_PLUGINCONTROL;
  ImageIndex := Ord(ctPlugin);
  KeyEnabled := True;
end;

procedure TAHMPluginControl.SendKey(KeyCode: String);
begin
  inherited;

  // Notify plugin of key press event
  if Assigned(FPlugin) then FPlugin.KeyPress(KeyCode);
end;

procedure TAHMPluginControl.SendKey(KeyCode: Word; Shift: TShiftState);
begin
  inherited;

  // Notify plugin of key press event
  if Assigned(FPlugin) then FPlugin.KeyPress(KeyCode, Shift);
end;

procedure TAHMPluginControl.Press;
var
  X, Y: Single;
begin
  inherited;

  // Notify plugin of mouse down event
  NormaliseMouseScale(X, Y);
  if Assigned(FPlugin) then FPlugin.MouseDown(X, Y);
end;

procedure TAHMPluginControl.UnPress;
var
  X, Y: Single;
begin
  inherited;

  // Notify plugin of mouse up event
  NormaliseMouseScale(X, Y);
  if Assigned(FPlugin) then FPlugin.MouseUp(X, Y);
end;

procedure TAHMPluginControl.MouseMoved(Down: Boolean);
var
  X, Y: Single;
begin
  inherited;

  // Notify plugin of mouse move event
  NormaliseMouseScale(X, Y);
  if Assigned(FPlugin) then FPlugin.MouseMove(X, Y, Down);
end;

class function TAHMPluginControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.PluginWindowList;
end;

//----------------------------------------------------------------------------//

constructor TAHMButtonControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_BUTTON;
  ImageIndex := Ord(ctButton);

  // Create a text scroller (free'd by ancestor class)
  Scroller := TAHMTextScroller.Create;
end;

class function TAHMButtonControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.ButtonList;
end;

//----------------------------------------------------------------------------//

constructor TAHMComboButtonControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_COMBOBUTTON;
  ImageIndex := Ord(ctComboButton);

  // Create a text scroller (free'd by ancestor class)
  Scroller := TAHMTextScroller.Create;
end;

procedure TAHMComboButtonControl.SeedObject;
begin
{$IFDEF DESIGNER}
  if Ancestor = nil then
  begin
    MetaValue := META_SCREENSAVER;
    MetaGroup := META_SCREENSAVERS;
  end;
{$ENDIF}
end;

procedure TAHMComboButtonControl.Activate;
begin
  inherited;

  // Cycle through combo values
  if (MetaValue <> '') and (MetaGroup <> '') then
    FunctionsInstance.NextMeta(MetaValue, MetaGroup);
end;

class function TAHMComboButtonControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.ButtonList;
end;

//----------------------------------------------------------------------------//

constructor TAHMControlTextParser.Create(Control: TAHMEditableControl);
begin
  inherited Create;

  FControl := Control;
end;

procedure TAHMControlTextParser.Enter;
begin
  inherited;

  // Inform editable control that user has entered new text
  FControl.TextEdited;
end;

//----------------------------------------------------------------------------//

constructor TAHMEditableControl.Create(const Name: String);
begin
  inherited Create(Name);

  FTextParser := TAHMControlTextParser.Create(Self);
  KeyEnabled := True;
end;

destructor TAHMEditableControl.Destroy;
begin
  FTextParser.Free;

  inherited;
end;

procedure TAHMEditableControl.UpdateTextParser;
begin
  if FDirtyText then
  begin
    // Lookup text/macro translation and load into parser
    FTextParser.Text := inherited GetDisplayText;
    FDirtyText := False;
  end;
end;

function TAHMEditableControl.GetDisplayText: String;
begin
  // Update text parser if text has changed
  UpdateTextParser;

  // Use display text from our parser whilst focussed
  if Focused then Result := FTextParser.DisplayText
             else Result := inherited GetDisplayText;
end;

procedure TAHMEditableControl.TextEdited;
begin
  // If we have a metadata value defined, then update it with entered text
  if MetaValue <> '' then
    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaValue, FTextParser.Text);
end;

procedure TAHMEditableControl.SendKey(KeyCode: String);
begin
  inherited;

  // Update text parser if text has changed
  UpdateTextParser;

  // Pass keycode to our text entry parser to handle
  FTextParser.ProcessKeyPress(KeyCode);
end;

procedure TAHMEditableControl.SendKey(KeyCode: Word; Shift: TShiftState);
begin
  inherited;

  // Update text parser if text has changed
  UpdateTextParser;

  // Pass keycode to our text entry parser to handle
  FTextParser.ProcessKeyPress(KeyCode, Shift);
end;

procedure TAHMEditableControl.Loaded;
begin
  inherited;

  // Flag our text as dirty so we initialise parser
  FDirtyText := True;
end;


//----------------------------------------------------------------------------//

constructor TAHMTextEditControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_TEXTEDIT;
  ImageIndex := Ord(ctTextEdit);
end;

class function TAHMTextEditControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.TextEditList;
end;

//----------------------------------------------------------------------------//

constructor TAHMCheckButtonControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_CHECKBUTTON;
  ImageIndex := Ord(ctCheckButton);

  // Set default meta group and index - true/false values
  MetaGroup := META_BOOLEANS;
  MetaIndex := META_BOOL_YES;
end;

function TAHMCheckButtonControl.GetChecked: Boolean;
begin
  // Check boolean state of our current meta value
  Result := ApplicationManagerInstance.MetaDataManager.GetMetaBoolean(MetaValue);
end;

procedure TAHMCheckButtonControl.SetChecked(Checked: Boolean);
begin
  // If our meta state doesn't match check state then update meta to match
  if (GetChecked <> Checked) and (MetaValue <> '') and (MetaGroup <> '') then
  begin
    // Cycle through combo values
    FunctionsInstance.NextMeta(MetaValue, MetaGroup);
  end;

  inherited;
end;

procedure TAHMCheckButtonControl.SeedObject;
begin
{$IFDEF DESIGNER}
  if Ancestor = nil then
  begin
    MetaGroup := META_BOOLEANS;
    MetaIndex := META_BOOL_YES;
    MetaValue := META_BOOLEAN_TEST;
  end;
{$ENDIF}
end;

procedure TAHMCheckButtonControl.SaveAsXML(Lines: TStrings);
begin
  // Hide meta settings whilst saving so they're not persisted
  MetaGroup := '';
  MetaIndex := '';
  try
    inherited;
  finally
    MetaGroup := META_BOOLEANS;
    MetaIndex := META_BOOL_YES;
  end;
end;

class function TAHMCheckButtonControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.ButtonList;
end;

//----------------------------------------------------------------------------//

constructor TAHMRadioButtonControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_RADIOBUTTON;
  ImageIndex := Ord(ctRadioButton);

  // Create a text scroller (free'd by ancestor class)
  Scroller := TAHMTextScroller.Create;
end;

function TAHMRadioButtonControl.GetDisplayMeta: String;
begin
  // Always use index value for display text (text doesn't change)
  Result := MetaIndex;
end;

function TAHMRadioButtonControl.GetChecked: Boolean;
var
  Value: String;
begin
  // Compare current meta value with our index meta value
  Value := ApplicationManagerInstance.MetaDataManager.GetMetaValue(MetaValue);
  Result := (Value = MetaIndex);
end;

procedure TAHMRadioButtonControl.SetChecked(Checked: Boolean);
begin
  // If our meta state doesn't matter check state then update meta to match
  if (GetChecked <> Checked) and (MetaValue <> '') and (MetaGroup <> '') then
  begin
    // Select/Cycle through combo values to reflect checked state
    if Checked then
      ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaValue, MetaIndex)
    else
      FunctionsInstance.NextMeta(MetaValue, MetaGroup);
  end;

  inherited;
end;

procedure TAHMRadioButtonControl.SeedObject;
begin
{$IFDEF DESIGNER}
  if Ancestor = nil then
  begin
    MetaGroup := META_BOOLEANS;
    MetaIndex := META_BOOL_YES;
    MetaValue := META_BOOLEAN_TEST;
  end;
{$ENDIF}
end;

procedure TAHMRadioButtonControl.Activate;
begin
  inherited;

  // Check the radio button by setting meta value to index
  if (MetaValue <> '') and (MetaIndex <> '') then
    FunctionsInstance.SetMeta(MetaValue, MetaIndex);
end;

class function TAHMRadioButtonControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.ButtonList;
end;

//----------------------------------------------------------------------------//

constructor TAHMGridControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_GRIDCONTROL;
  ImageIndex := Ord(ctGrid);

  // Create storage list for our grid contents
  FGridObjects := TAHMGridObjectList.Create;

  // Create a text scroller (free'd by ancestor class)
  Scroller := TAHMTextScroller.Create;
end;

destructor TAHMGridControl.Destroy;
begin
  FGridObjects.Free;

  inherited Destroy;
end;

function TAHMGridControl.GetChecked: Boolean;
var
  Obj: TAHMGridObject;
begin
  // Determine checked state for selected grid object (if any)
  Obj := GetGridObject(FSelectedRow, FSelectedCol);
  if Assigned(Obj) then Result := Obj.Checked else Result := False;
end;

procedure TAHMGridControl.SetChecked(Checked: Boolean);
var
  Obj: TAHMGridObject;
begin
  // Update checked state for selected grid object (if any)
  Obj := GetGridObject(FSelectedRow, FSelectedCol);
  if Assigned(Obj) then Obj.Checked := Checked;
end;

procedure TAHMGridControl.RefreshGridContents;
begin
  // Clear current grid contents
  FGridObjects.Clear;

  // Position cursor on a valid cell (hopefully)
  FSelectedRow := 0;
  FSelectedCol := 0;

  // Scroll grid to top left corner
  FLeftCol := 0;
  FTopRow := 0;
  SnapToPosition;

  // Now we can repopulate our grid contents
  PopulateGridContents;
end;

procedure TAHMGridControl.PopulateGridContents;
var
  i, Count: Integer;
  sSelect, sFocus, sUnFocus: String;
begin
  // Default implementation to populate grid contents from metadata list
  if (MetaGroup <> '') and (MetaIndex <> '') then
  begin
    // Get count of associated metadata items
    Count := ApplicationManagerInstance.MetaDataManager.GetMetaCount(MetaGroup);

    // Add each metadata entry
    for i := 0 to Pred(Count) do
      with FGridObjects.AddObject do
        with ApplicationManagerInstance.MetaDataManager do
        begin
          // Add text and image properties
          Index := IntToStr(i);
          WideIndex := Index;
          Text := GetIndexedMetaValue(MetaGroup, MetaIndex, i);
          ImageURL := GetIndexedMetaValue(MetaGroup, META_GRID_IMAGE_URL, i);
          ImageIndex := GetIndexedMetaValue(MetaGroup, META_GRID_IMAGE_INDEX, i);
          Description := GetIndexedMetaValue(MetaGroup, META_GRID_ITEM_DESC, i);
          Data := GetIndexedMetaValue(MetaGroup, META_GRID_ITEM_DATA, i);
          Value := Text;

          // Check for any event action definitions
          sSelect  := GetIndexedMetaValue(MetaGroup, META_GRID_SELECT_FUNCTION, i);
          sFocus   := GetIndexedMetaValue(MetaGroup, META_GRID_FOCUS_FUNCTION, i);
          sUnFocus := GetIndexedMetaValue(MetaGroup, META_GRID_UNFOCUS_FUNCTION, i);
          if (sSelect <> '') or (sFocus <> '') or (sUnFocus <> '') then
          begin
            AddActions(sSelect,
                       GetIndexedMetaValue(MetaGroup, META_GRID_SELECT_TARGET, i),
                       GetIndexedMetaValue(MetaGroup, META_GRID_SELECT_PARAM, i),
                       sFocus,
                       GetIndexedMetaValue(MetaGroup, META_GRID_FOCUS_TARGET, i),
                       GetIndexedMetaValue(MetaGroup, META_GRID_FOCUS_PARAM, i),
                       sUnFocus,
                       GetIndexedMetaValue(MetaGroup, META_GRID_UNFOCUS_TARGET, i),
                       GetIndexedMetaValue(MetaGroup, META_GRID_UNFOCUS_PARAM, i));
          end;
        end;
  end;
end;

procedure TAHMGridControl.Loaded;
begin
  inherited Loaded;

  // Populate grid contents
  RefreshGridContents;

  // Set flag to indicate we have loaded contents
  FLoadedContents := True;

  // Adjust grid row & column counts to suit our grid items
  with DefaultStyle as TAHMGrid do
  begin
    FRowCount := RowCount;
    FColCount := ColumnCount;

    // Scroll vertically if we have more than one row
    FVerticalScroll := (FRowCount > 1);

    // If scrolling vertically stretch rows, otherwise stretch columns
    if FVerticalScroll and (FColCount > 0) then
      FRowCount := Ceil(FGridObjects.Count / FColCount)
    else if FRowCount > 0 then
      FColCount := Ceil(FGridObjects.Count / FRowCount);
  end;

  // If we have a previously stored grid item then try to highlight it
  if MetaValue <> '' then
    SetGridObject(ApplicationManagerInstance.MetaDataManager.GetMetaValue(MetaValue))
  else
    SetGridObject('');
end;

procedure TAHMGridControl.InvalidateStyle;
begin
  inherited;

  // If we change style we will need to reload the items so we resize images
  FLoadedContents := False;
end;

procedure TAHMGridControl.InitialiseEffect;
begin
  // Suppress standard effect rendering - this takes place in grid object
end;

procedure TAHMGridControl.RenderEffects;
begin
  // Suppress standard effect rendering - this takes place in grid object
end;

procedure TAHMGridControl.SetGridObject(ItemIndex: String; Validate: Boolean);
var
  i: Integer;
  Row, Column: Integer;
begin
  // First check to see if we have specified item loaded in our object list
  if ItemIndex <> '' then i := FGridObjects.LookupObjectIndex(ItemIndex)
                     else i := -1;
  if i <> -1 then
  begin
    // Determine corresponding row & column position for that item
    Row := i div FColCount;
    Column := i mod FColCount;
  end
  else
  begin
    // Not found, if validation switched on then exit now
    if Validate then Exit;

    // No validation, so just select top left item
    Row := 0;
    Column := 0;
  end;

  // Ensure selected cell is visible onscreen and that we fire scroll actions
  ScrollToSelection(Row, Column);
end;

function TAHMGridControl.GetGridObject(Row, Column: Integer): TAHMGridObject;
begin
  Result := FGridObjects.Objects[(Row * FColCount) + Column];
end;

function TAHMGridControl.UpdateMouseSelection: Boolean;
var
  Row, Col: Integer;
begin
  // Set default result - selection failed
  Result := False;

  with DefaultStyle as TAHMGrid do
  begin
    // Sanity check row/column size to prevent division by zero
    if (RowHeight = 0) or (ColumnWidth = 0) then Exit;

    // Calculate row/column under mouse cursor
    Row := Ceil((MouseY - FRowOffset) / RowHeight);
    Col := Ceil((MouseX - FColOffset) / ColumnWidth);

    // Ignore mouse move over hidden   grid items at boundary
    if (Row > VisibleRows) or (Col > VisibleColumns) then Exit;
    if (MouseX < ColumnMargin) or (MouseY < RowMargin) then Exit;

    // Select item at this row/column (if valid)
    Result := UpdateSelection(FTopRow + Row - 1, FLeftCol + Col - 1);
  end;
end;

function TAHMGridControl.UpdateSelection(Row, Column: Integer): Boolean;
var
  OldObj, NewObj: TAHMGridObject;
  ImageURL: WideString;
  Bitmap: TAHMBitmap;
  Index: Integer;
begin
  // Set default result - can't update selection
  Result := False;

  // First check we have a grid object at passed coordinates
  NewObj := GetGridObject(Row, Column);
  if not Assigned(NewObj) then Exit;
  OldObj := GetGridObject(FSelectedRow, FSelectedCol);

  // If we have a meta value name defined then update its value with position
  if MetaValue <> '' then
    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaValue, NewObj.Index);

  // If we have a meta index value defined then update grid context information
  if MetaIndex <> '' then
  begin
    Index := (Row * FColCount) + Column;

    // Lookup shared bitmap filename if using default images
    ImageURL := NewObj.ImageURL;
    if (ImageURL = '') and (NewObj.ImageIndex <> '') then
      with DefaultStyle as TAHMGrid do
      begin
        Bitmap := TAHMBitmap(DefaultImages.GetState(NewObj.ImageIndex));
        if Bitmap <> nil then ImageURL := Bitmap.FileURL;
      end;

    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_PREFIX, NewObj.Prefix);
    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_TEXT, NewObj.Text);
    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_DESCR, NewObj.Description);
    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_DATA, NewObj.Data);
    ApplicationManagerInstance.MetaDataManager.SetMetaWideValue(MetaIndex + META_GRID_IMAGE, ImageURL);
    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_INDEX, IntToStr(Index));
    ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_ITEMS, IntToStr(FGridObjects.Count));
  end;

  // Fire actions if we change focus
  if (OldObj <> NewObj) and Assigned(OldObj) and Assigned(OldObj.Actions) then
    OldObj.Actions.ProcessUnFocus;
  if (OldObj <> NewObj) and Assigned(NewObj) and Assigned(NewObj.Actions) then
    NewObj.Actions.ProcessFocus;

  // Save new selection position & success result
  FSelectedRow := Row;
  FSelectedCol := Column;
  Result := True;
end;

function TAHMGridControl.ScrollToSelection(Row, Column: Integer): Boolean;
begin
  // First ensure row & column coordinates point to a valid object
  Result := UpdateSelection(Row, Column);

  if Result then
    with DefaultStyle as TAHMGrid do
    begin
      // Ensure selected cell is in view
      if Row < FTopRow then FTopRow := Row;
      if Column < FLeftCol then FLeftCol := Column;
      if Row > FTopRow + Pred(VisibleRows) then FTopRow := Row - Pred(VisibleRows);
      if Column > FLeftCol + Pred(VisibleColumns) then FLeftCol := Column - Pred(VisibleColumns);

      // Snap smooth scrolling to nearest row/column
      SnapToPosition;
      
      // Fire any actions required after scrolling
      FireScrollActions;
    end;
end;

procedure TAHMGridControl.SnapToPosition;
begin
  // Snap partially scrolled row/column to nearest one
  FRowOffset := 0.0;
  FColOffset := 0.0;
  FScrollSpeed := 0.0;
end;

procedure TAHMGridControl.UpdateSmoothScrolling;
var
  TimeFactor, Delta: Single;
  ScrollActions, ScrollBounds: Boolean;
begin
  // Calculate frame duration variation from our ideal (50ms) interval
  TimeFactor := TimerInstance.CurrentFrameTime / EXPECTED_INTERVAL;

  // Calculate change in scroll speed
  Delta := ((Abs(FScrollSpeed) * DECEL_FACTOR) + DECEL_SPEED) * TimeFactor;

  // Brake scroll speed over time, then stop when we get to very low speed
  if FScrollSpeed > MIN_SPEED then
     if FScrollSpeed > Delta then FScrollSpeed := FScrollSpeed - Delta
                             else FScrollSpeed := 0.0
  else if FScrollSpeed < -MIN_SPEED then
     if FScrollSpeed < -Delta then FScrollSpeed := FScrollSpeed + Delta
                              else FScrollSpeed := 0.0
  else FScrollSpeed := 0.0;

  // If we are scrolling then we'll need to fire our scrolling actions
  ScrollActions := (FScrollSpeed <> 0.0);

  // Apply scrolling speed to scroll offset for our inertia effect
  if FScrollBraking then
  begin
    if FVerticalScroll then ScrollBounds := SmoothScroll(0, FScrollSpeed * TimeFactor)
                       else ScrollBounds := SmoothScroll(FScrollSpeed * TimeFactor, 0);
    // Stop scrolling if we moved past boundaries
    if not ScrollBounds then FScrollSpeed := 0.0;
  end;

  // Fire any actions required after scrolling
  if ScrollActions then FireScrollActions;
end;

procedure TAHMGridControl.FireScrollActions;
var
  sUp, sDn, sLf, sRt: String;
  Page, Pages, Index: Integer;
begin
  with DefaultStyle as TAHMGrid do
  begin
    // See whether its possible to scroll in each direction
    if FTopRow > 0 then sUp := AC_MOREUP else sUp := AC_NONEUP;
    if FLeftCol > 0 then sLf := AC_MORELF else sLf := AC_NONELF;
    if FTopRow < (FRowCount - VisibleRows) then sDn := AC_MOREDN else sDn := AC_NONEDN;
    if FLeftCol < (FColCount - VisibleColumns) then sRt := AC_MORERT else sRt := AC_NONERT;

    // If we have a meta index value defined then update grid context information
    if MetaIndex <> '' then
    begin
      // Fix compiler hint
      Page := 0;
      Pages := 0;

      // First - are we using horizontal or vertical pagecount/scrolling?
      if FVerticalScroll and (VisibleRows > 0) then
      begin
        Pages := Ceil(FRowCount / VisibleRows);
        Page := Succ(Ceil(FTopRow / VisibleRows));
      end
      else if (not FVerticalScroll) and (VisibleColumns > 0) then
      begin
        Pages := Ceil(FColCount / VisibleColumns);
        Page := Succ(Ceil(FLeftCol / VisibleColumns));
      end;

      Index := (FSelectedRow * FColCount) + FSelectedCol;

      // Update selected grid context information
      ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_PAGE, IntToStr(Page));
      ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_PAGES, IntToStr(Pages));
      ApplicationManagerInstance.MetaDataManager.SetMetaValue(MetaIndex + META_GRID_INDEX, IntToStr(Index));
    end;
  end;

  // Fire appropriate action for each direction
  Actions.InvokeAction(sUp);
  Actions.InvokeAction(sDn);
  Actions.InvokeAction(sLf);
  Actions.InvokeAction(sRt);
end;

function TAHMGridControl.PageScroll(Direction: TAHMDirection): Boolean;
var
  Row, Col: Integer;
begin
  // Set default result - scrolled
  Result := True;

  with DefaultStyle as TAHMGrid do
  begin
    // See if we can scroll in requested direction
    case Direction of
      diUp: if FTopRow > 0 then Dec(FTopRow, VisibleRows) else Result := False;
      diDown: if FTopRow < (FRowCount - VisibleRows) then Inc(FTopRow, VisibleRows) else Result := False;
      diLeft: if FLeftCol > 0 then Dec(FLeftCol, VisibleColumns) else Result := False;
      diRight: if FLeftCol < (FColCount - VisibleColumns) then Inc(FLeftCol, VisibleColumns) else Result := False;
    end;

    // If we can't scroll, then can we move selection to first/last row/column?
    if not Result then
    begin
      case Direction of
        diUp: if FSelectedRow > 0 then UpdateSelection(0, FSelectedCol) else Exit;
        diDown: if FSelectedRow < Pred(FRowCount) then UpdateSelection(Pred(FRowCount), FSelectedCol) else Exit;
        diLeft: if FSelectedCol > 0 then UpdateSelection(FSelectedRow, 0) else Exit;
        diRight: if FSelectedCol < Pred(FColCount) then UpdateSelection(FSelectedRow, Pred(FColCount)) else Exit;
      end;
    end;

    // Ensure we don't scroll past edges of grid
    if FTopRow > (FRowCount - VisibleRows) then FTopRow := (FRowCount - VisibleRows);
    if FLeftCol > (FColCount - VisibleColumns) then FLeftCol := (FColCount - VisibleColumns);
    if FTopRow < 0 then FTopRow := 0;
    if FLeftCol < 0 then FLeftCol := 0;

    // Move selection if necessary so that it is visible on new page
    Row := FSelectedRow;
    Col := FSelectedCol;
    if Row < FTopRow then Row := FTopRow;
    if Col < FLeftCol then Col := FLeftCol;
    if Row > FTopRow + Pred(VisibleRows) then Row := FTopRow + Pred(VisibleRows);
    if Col > FLeftCol + Pred(VisibleColumns) then Col := FLeftCol + Pred(VisibleColumns);
    while (not UpdateSelection(Row, FLeftCol)) and (Row >= FTopRow) do Dec(Row);
    while (not UpdateSelection(Row, Col)) and (Col >= FLeftCol) do Dec(Col);
  end;

  // Fire any actions required after scrolling
  FireScrollActions;
  DidSomething;

  // Scrolled successfully
  Result := True;
end;

function TAHMGridControl.Scroll(Direction: TAHMDirection): Boolean;
var
  Row, Col: Integer;
begin
  // Set default result - not scrolled
  Result := False;

  // Make a note of current selected row/column
  Row := FSelectedRow;
  Col := FSelectedCol;

  // See if we can scroll in requested direction
  case Direction of
    diUp: if Row > 0 then Dec(Row) else Exit;
    diDown: if Row < Pred(RowCount) then Inc(Row) else Exit;
    diLeft: if Col > 0 then Dec(Col) else Exit;
    diRight: if Col < Pred(ColumnCount) then Inc(Col) else Exit;
  end;

  // Abort if no grid object at new selected coordinate
  Result := ScrollToSelection(Row, Col);
  if Result and (Direction <> diNone) then DidSomething;
end;

function TAHMGridControl.SmoothScroll(DX, DY: Single): Boolean;
var
  LastRow, LastCol: Integer;
begin
  // Set default result - scroll permitted
  Result := True;

  // Apply scrolling deltas to scroll offset
  FRowOffset := FRowOffset + DY;
  FColOffset := FColOffset + DX;

  // Adjust grid row/column position for large offsets
  with DefaultStyle as TAHMGrid do
    if FVerticalScroll then
    begin
      while FRowOffset > RowHeight do
      begin
        Dec(FTopRow);
        FRowOffset := FRowOffset - RowHeight;
      end;
      while FRowOffset < -RowHeight do
      begin
        Inc(FTopRow);
        FRowOffset := FRowOffset + RowHeight;
      end;
    end
    else
    begin
      while FColOffset > ColumnWidth do
      begin
        Dec(FLeftCol);
        FColOffset := FColOffset - ColumnWidth;
      end;
      while FColOffset < -ColumnWidth do
      begin
        Inc(FLeftCol);
        FColOffset := FColOffset + ColumnWidth;
      end;
    end;

  // Check we haven't gone past grid boundaries - stop scrolling if we do
  with DefaultStyle as TAHMGrid do
    if FVerticalScroll then
    begin
      LastRow := FRowCount - VisibleRows;

      if (LastRow < 1) or (FTopRow < 0) or ((FTopRow = 0) and (FRowOffset > 0.0)) then
      begin
        FTopRow := 0;
        FRowOffset := 0.0;
        Result := False;
      end
      else if (FTopRow > LastRow) or ((FTopRow = LastRow) and (FRowOffset < 0.0)) then
      begin
        FTopRow := LastRow;
        FRowOffset := 0.0;
        Result := False;
      end;
    end
    else
    begin
      LastCol := FColCount - VisibleColumns;

      if (LastCol < 1) or (FLeftCol < 0) or ((FLeftCol = 0) and (FColOffset > 0.0)) then
      begin
        FLeftCol := 0;
        FColOffset := 0.0;
        Result := False;
      end
      else if (FLeftCol > LastCol) or ((FLeftCol = LastCol) and (FColOffset < 0.0)) then
      begin
        FLeftCol := LastCol;
        FColOffset := 0.0;
        Result := False;
      end;
    end;
end;

procedure TAHMGridControl.Activate;
var
  Obj: TAHMGridObject;
begin
  inherited;

  // Determine actions for selected grid object (if any)
  Obj := GetGridObject(FSelectedRow, FSelectedCol);
  if Assigned(Obj) and Assigned(Obj.Actions) then
    Obj.Actions.ProcessSelect;
end;

procedure TAHMGridControl.MouseClicked;
begin
  // Start scroll braking effect when mouse released
  FScrollBraking := True;

  // Only process click actions if we clicked a valid grid item
  if (Abs(MouseX - MouseDownX) < ScreenInstance.ClickRadius) and
     (Abs(MouseY - MouseDownY) < ScreenInstance.ClickRadius) and
     UpdateMouseSelection then
    inherited;
end;

procedure TAHMGridControl.MouseMoved(Down: Boolean);
begin
  inherited;

  // Track gesture if mouse is down, otherwise update selection
  if Down then
  begin
    // Accelerate scroll speed in direction of mouse movement
    if FVerticalScroll then
      FScrollSpeed := FScrollSpeed + (MouseDeltaY * ACCEL_FACTOR)
    else
      FScrollSpeed := FScrollSpeed + (MouseDeltaX * ACCEL_FACTOR);

    // Clamp maximum scroll speed in each direction
    if FScrollSpeed > MAX_SPEED then FScrollSpeed := MAX_SPEED;
    if FScrollSpeed < -MAX_SPEED then FScrollSpeed := -MAX_SPEED;

    // Drag grid position using mouse whilst button held
    if FVerticalScroll then SmoothScroll(0, MouseDeltaY)
                       else SmoothScroll(MouseDeltaX, 0);
    FScrollBraking := False;
  end
  else UpdateMouseSelection;
end;

procedure TAHMGridControl.MouseWheel(Delta: Integer);
begin
  inherited;

  // Use mouse wheel to scroll grid a page at a time
  if FVerticalScroll then
  begin
    // Scroll vertically
    if Delta > 0 then PageScroll(diUp) else if Delta < 0 then PageScroll(diDown);
  end
  else
  begin
    // Scroll horizontally
    if Delta > 0 then PageScroll(diLeft) else if Delta < 0 then PageScroll(diRight);
  end
end;

procedure TAHMGridControl.Idle;
var
  Obj: TAHMGridObject;
  CurrentIndex, MetaIndex: String;
begin
  inherited;

  // Check for external changes to our MetaValue (current item URL)
  if MetaValue <> '' then
  begin
    // Get selected grid object & external metadata index
    Obj := GetGridObject(FSelectedRow, FSelectedCol);
    if Assigned(Obj) then CurrentIndex := Obj.Index else CurrentIndex := '';
    MetaIndex := ApplicationManagerInstance.MetaDataManager.GetMetaValue(MetaValue);

    // Select new URL if its different and exists
    if (MetaIndex <> '') and (MetaIndex <> CurrentIndex) then
      SetGridObject(MetaIndex, True);
  end;
end;

procedure TAHMGridControl.Invalidate;
begin
  // Reload grid contents if invalidated, but only once control initialised
  if ControlLoaded then Loaded;
end;

procedure TAHMGridControl.Rendering;
var
  Index, Row, Col: Integer;
begin
  inherited;

  // Update grid smooth scrolling each time we render
  UpdateSmoothScrolling;

  // We need to check for any external changes to our grid metadata
  if (MetaIndex <> '') and (FColCount > 0) then
  begin
    // Respond to grid index position changes and scroll value in view
    Index := ApplicationManagerInstance.MetaDataManager.GetMetaInteger(MetaIndex + META_GRID_INDEX);
    Row := Index div FColCount;
    Col := Index mod FColCount;
    if (Row <> FSelectedRow) or (Col <> FSelectedCol) then
      ScrollToSelection(Row, Col);
  end;

  // Backup our scroller property before we render
  FPrivateScroller := Scroller;
end;

procedure TAHMGridControl.Rendered;
begin
  // Restore private scroller after rendering
  Scroller := FPrivateScroller;
end;

procedure TAHMGridControl.UpdateScroller(FocusedItem: Boolean);
begin
  // Expose our scroller if rendering focussed item
  if FocusedItem then Scroller := FPrivateScroller
                 else Scroller := nil;
end;

class function TAHMGridControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.GridList;
end;

//----------------------------------------------------------------------------//

constructor TAHMMediaGridControl.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_MEDIAGRIDCONTROL;
  FMediaType := mtUnknown;
end;

function TAHMMediaGridControl.GetMediaList: TAHMPlaylist;
begin
  // Determine appropriate playlist we wish to display - normally
  // common selection playlist in media manager unless we want to
  // view contents of the actual play queue
  with TAHMMediaGrid(DefaultStyle) do
    if FMediaType = mtPlaylist then
      Result := ApplicationManagerInstance.MediaManager.GetPlaylist(StreamType)
    else
      Result := ApplicationManagerInstance.MediaManager.MediaSelection;
end;

procedure TAHMMediaGridControl.RetrieveMediaItems;
var
  SortDir, SortName, Shuffle: Boolean;
begin
  with ApplicationManagerInstance.MetaDataManager do
  begin
    // Determine sort direction and type settings
    Shuffle := GetMetaBoolean(MetaIndex + META_GRID_SHUFFLE);
    SortDir := GetMetaBoolean(MetaIndex + META_GRID_SORTASC);
    SortName := GetMetaValue(MetaIndex + META_GRID_SORTTYPE) <> META_SORT_DATE;
  end;

  // Set required media list sort direction and type
  with MediaList do
  begin
    if SortDir then SortDirection := sdAscending else SortDirection := sdDescending;
    if Shuffle then SortType := stShuffle
    else if SortName then SortType := stAlpha
    else SortType := stDate
  end;

  // If we already have grid contents loaded just resort
  if LoadedContents and (FFolder = FPrevFolder) then
    MediaList.Sort
  else
  begin
    // Browse appropriate media content type from media repository
    case FMediaType of
      mtImage: ApplicationManagerInstance.MediaFilesManager.BrowseImages(FFolder, MediaList);
      mtAudio: ApplicationManagerInstance.MediaFilesManager.BrowseMusic(FFolder, MediaList);
      mtVideo: ApplicationManagerInstance.MediaFilesManager.BrowseVideos(FFolder, MediaList);
      mtRadio: ApplicationManagerInstance.MediaFilesManager.BrowseRadio(FFolder, MediaList);
      else ApplicationManagerInstance.MediaFilesManager.BrowseMedia(FFolder, MediaList);
    end;

    // If we are showing no media at the root level then fire our no media event
    if (FFolder = '') and MediaList.Empty then
      ApplicationManagerInstance.ActionManager.ProcessAction(AC_NOMEDIA);
  end;
end;

procedure TAHMMediaGridControl.PopulateGridContents;
var
  i: Integer;
  MediaItem: TAHMMediaItem;
begin
  // Attempt to recover our previous folder setting from metagroup property
  if (not FLoadedContents) and (MetaGroup <> '') then
    FFolder := ApplicationManagerInstance.MetaDataManager.GetMetaWideValue(MetaGroup);

  // Retrieve and cache media type from grid style
  with TAHMMediaGrid(DefaultStyle) do
  begin
    // If type has changed after loading then reset to root folder
    if (FMediaType <> mtUnknown) and (FMediaType <> MediaType) then FFolder := '';
    FMediaType := MediaType;
  end;

  // Retrieve media items for grid (unless just browsing playlist)
  if FMediaType <> mtPlaylist then RetrieveMediaItems;

  // Store our new folder and write to metadata property in case we unload
  // the control - this is queued using an action rather than directly updating
  // metadata as we may wish to process conditional actions on the current
  // folder within the theme. Note UTF8 encoding of filename for unicode chars
  FPrevFolder := FFolder;
  if MetaGroup <> '' then
    ApplicationManagerInstance.ActionManager.EnqueueAction(FN_SETMETA, MetaGroup, EncodeWideString(FFolder));

  // Populate grid objects from playlist contents
  for i := 0 to Pred(MediaList.Count) do
    with FGridObjects.AddObject do
    begin
      MediaItem := MediaList.Items[i];
      Index := MediaItem.URL;
      WideIndex := MediaItem.URL;
      Text := MediaItem.Caption;
      Value := MediaItem.Name;
      ImageURL := MediaItem.Icon;
      ImageIndex := GetMediaItemImageIndex(MediaItem.MediaType);
      if MediaItem.MediaType in [mtAudio, mtFolder] then
      begin
        Description := MediaItem.Author;
        if MediaItem.Duration > 0 then
          Data := TimerInstance.SecondsToString(MediaItem.Duration, False);
      end;
    end;
end;

function TAHMMediaGridControl.UpdateSelection(Row, Column: Integer): Boolean;
var
  GridObj: TAHMGridObject;
  MediaObj: TAHMMediaItem;
begin
  Result := inherited UpdateSelection(Row, Column);

  // Don't update selection if we're just browsing a playlist
  if Result and (FMediaType <> mtPlaylist) then
  begin
    // Get grid object for new media selection and lookup associated media item
    GridObj := GetGridObject(Row, Column);
    MediaObj := MediaList.FindItemByURL(GridObj.WideIndex);

    // Tell media manager that we have selected a new media item
    ApplicationManagerInstance.MediaManager.MediaItem := MediaObj;
  end;
end;

procedure TAHMMediaGridControl.Activate;
var
  Obj: TAHMGridObject;
  MediaType: TAHMMediaType;
  ParentDir: WideString;
begin
  inherited;

  // Determine properties for selected grid object (if any)
  Obj := GetGridObject(FSelectedRow, FSelectedCol);
  if not Assigned(Obj) then Exit;
  MediaType := GetMediaType(Obj.WideIndex);

  // Navigate to folder or fire action event on selection...
  if MediaType = mtFolder then
  begin
    // Append path to current folder
    FFolder := FFolder + Obj.WideIndex
  end
  else if MediaType = mtParent then
  begin
    // Parent path is already stored in grid object
    FFolder := Obj.Value;

    // Save parent directory in metadata so we highlight it on reloading grid
    if MetaValue <> '' then
    begin
      // Extract folder minus trailing '..' characters
      ParentDir := Copy(Obj.WideIndex, 1, Length(Obj.WideIndex) - 2);
      ApplicationManagerInstance.MetaDataManager.SetMetaWideValue(MetaValue, ParentDir);
    end;
  end
  else
  begin
    // No need to reload grid contents if just making a selection
    Actions.InvokeAction(AC_SELECT);
    Exit;
  end;

  // Repopulate media listing from new folder
  Loaded;
end;

procedure TAHMMediaGridControl.Cancel;
begin
  inherited Cancel;

  // Use method below to move to parent folder
  Parent;
end;

procedure TAHMMediaGridControl.Parent;
var
  Obj: TAHMGridObject;
  ParentDir: WideString;
begin
  // We can only perform parent if not already at root level
  if FFolder = '' then Exit;

  Obj := GetGridObject(0, 0); // First item should always be "parent" item
  if Assigned(Obj) then
  begin
    // Parent path is already stored in grid object
    FFolder := Obj.Value;

    // Save parent directory in metadata so we highlight it on reloading grid
    if MetaValue <> '' then
    begin
      // Extract folder minus trailing '..' characters
      ParentDir := Copy(Obj.WideIndex, 1, Length(Obj.WideIndex) - 2);
      ApplicationManagerInstance.MetaDataManager.SetMetaWideValue(MetaValue, ParentDir);
    end;
  end;

  // Repopulate media listing from new folder
  Loaded;
end;

procedure TAHMMediaGridControl.Root;
begin
  // Only move to root if not already there
  if FFolder <> '' then
  begin
    FFolder := '';

    // Repopulate media listing from new folder
    Loaded;
  end;
end;

class function TAHMMediaGridControl.GetStyleList: TAHMUIObjectList;
begin
  // Lookup appropriate style object in visual descriptor
  Result := ApplicationManagerInstance.ThemeManager.VisualDescriptor.MediaGridList;
end;


end.
